#!/usr/bin/env python

import os
import gzip
import errno
import time
import select
import socket
import os.path
import collections

''' TODO: set BASEPATH '''

ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)

BASEPATH = '/home/idd/hotmocha'  
ERR_FMT_ERR		= -1
ERR_TOO_LONG		= -2
ERR_INTERNAL_ERR	= -3
ERR_PERR_CLOSED		= -4
ERR_READ_ERR		= -5
ERR_WRITE_ERR		= -6
ERR_OPEN_ERR		= -7
ERR_RESP_ERR		= -8

PROMPT_ENDS	= '>'
CDOK		= 'CDOK'
CDFAIL		= 'CDFAIL'

CMD_PUT         = 1
CMD_GET         = 2
CMD_CD          = 3
CMD_LS          = 4

MUTIIO_READ 	= 	0x01
MUTIIO_WRITE 	= 	0x02
MUTIIO_ERROR	= 	0x04
def readfile(filename, start = None, end = None):
        with open(filename, "rb") as file:
		if start is not None:
			file.seek(start)
		if end is not None:
			remaining = end - (start or 0)	
		else:
			remaining = None
                while True:
			chunk_size = 8 * 1024
			if remaining is not None and remaining < chunk_size:
				chunk_size = remaining	

                        chunk = file.read(chunk_size)
                        if chunk:
				if remaining is not None:
					remaining -= len(chunk)
				
                                yield chunk
                        else:
				if remaining is not None:
					raise TypeError('')
                                return

class log(object):
	def __init__(self, logfilename):
		self.logfilename = logfilename

	def log(self, str):
		print str
		with open(self.logfilename, "ab+") as file:
			file.write(str + '\n')
			file.flush()

L = log("./server.log")	

def geterrnofrome(e):
	if hasattr(e, 'errno'):
		return e.errno
	elif e.args:
		return e.args[0]
	else:
		return None

def mergeprefix(deque, size):
	if len(deque) == 1 and len(deque[0]) <= size:
		return
	prefix = []
	remaining = size
	while deque and remaining > 0:
		chunk = deque.popleft()
		if len(chunk) > remaining:
		    deque.appendleft(chunk[remaining:])
		    chunk = chunk[:remaining]
		prefix.append(chunk)
		remaining -= len(chunk)
	if prefix:
		deque.appendleft(type(prefix[0])().join(prefix))
	if not deque:
		deque.appendleft(b"")


# bufdeque is a collections.deque
'''
RETURN: 	
	0			: peer closed 
	ERR_READ_ERR		: 	
	>0			: read len
'''
def readfromsock(sock, bufdeque):
	chunklen = 0
	chunkdata = ''
	l = ''
	while True:
		try:
			chunkdata = sock.recv(8 * 1024)	
			l = len(chunkdata)
			chunklen += l
			if l == 0:	return 0 
		except (IOError, OSError) as e:
			if geterrnofrome(e) in ERRNO_WOULDBLOCK:
				break
			else:
				return ERR_READ_ERR

	#L.log('readfrom sock, data: ' + chunkdata + ' len: ' + str(chunklen))
	if chunklen != 0:
		bufdeque.append(chunkdata)

	return chunklen 
	
# bufdeque is a collections.deque
'''
RETURN:
        0                       : peer closed
        ERR_WRITE_ERR           :
        >0                      : send len
'''
def sendtosock(sock, bufdeque):
	sendlen = 0
	numbytes = 100
	while bufdeque:
		try:
			if bufdeque[0] is None or len(bufdeque[0]) == 0:
				mergeprefix(bufdeque, numbytes)
				bufdeque.popleft()

			numbytes = sock.send(bufdeque[0])
			if numbytes == 0: 	return ERR_WRITE_ERR		# attention: error

			sendlen += numbytes
			
			mergeprefix(bufdeque, numbytes)
			bufdeque.popleft()

		except (IOError, OSError) as e:
			if geterrnofrome(e) in ERRNO_WOULDBLOCK:
				break
			else:   
				return ERR_WRITE_ERR
	return sendlen

# sync write
''' 
RETURN:	write len 
'''
def writetofile(file, bufdeque):
	writelen = 0

	while bufdeque:
		if bufdeque[0] is None or len(bufdeque[0]) == 0:
			bufdeque.popleft()
			continue
		file.write(bufdeque[0])
		writelen += len(bufdeque[0])	
		bufdeque.popleft()

	file.flush()
	return writelen

'''
RETURN: read len
'''
def readfromfile(file, bufdeque):
	readdata = file.read(8 * 1024)
	bufdeque.append(readdata)
	return len(readdata)


class commandaction(object):
	def __init__(self, cmdtype, session, cmdline):
		self.cmdtype = cmdtype
		self.session = session
		self.cmdline = cmdline 
		self.callback = None

	def start(self):
		raise NotImplementedError("not implements start function")

	def parsecmdline(self):
		raise NotImplementedError("not implements parse commandline function")

	def startaction(self):
		raise NotImplementedError("not implements startaction function")
	
	def endaction(self, successflag = True):
		if successflag == True:
			L.log(self.session.cmdline + ' SUCCESS!')
		else:
			L.log(self.session.cmdline + ' FAILED!')
			
		''' force clear command line buf '''
		self.session.regetcommand()
		MUTIIO.register(self.session.sockfd, MUTIIO_READ)
		MUTIIO.unregister(self.session.sockfd, MUTIIO_WRITE)
		self.callback = self.session.handler_receivecmdline

	def sendreponse(self):
		numbytes = sendtosock(self.session.sock, self.session.outbuf)
		if numbytes <= 0:
			return ERR_WRITE_ERR 

''' 	base class member:
	cmdtype, session, cmdline, callback 
	1. client send command [put filename] and client waiting for server's getfileinfo response
	2. server parse command
	3. server send get fileinfo ok ['FILEINFO OK']
	4. client get fileinfo's response and block to send server filedata
	5. server receive filedata 
	6. when sendlen == filelen, server send response 'FILETRANSFER OK' to client
'''
class putfileaction(commandaction):
	RESPONSE_GETFILEINFO_OK = 'FILEINFO OK\n'
	RESPONSE_TRANSFERDATA_OK = 'FILETRANSFER OK\n'

	def __init__(self, cmdname, session, cmdline):
		super(putfileaction, self).__init__(cmdname, session, cmdline)
		self.file = None
		self.filename = ''
		self.filelen = 0
	
	def parsecmdline(self):
		L.log('putfile parsecmdline: ' + self.cmdline)
		x = self.cmdline.split(' ')
		if len(x) <= 1:
			L.log('command:' + self.cmdline + 'format error')
			return ERR_FMT_ERR 
		else:
			fileinfo = x[1].split(':')
			if len(fileinfo) <= 1:
				L.log('command:' + self.cmdline + 'format error')
				return ERR_FMT_ERR 
			else:
				self.filename = fileinfo[0]
				self.filelen = int(fileinfo[1])
				L.log('put cmdline: ' + self.filename + "  " + str(self.filelen))
		L.log('filename:' + self.filename + ' filelen: ' + str(self.filelen))

	def startaction(self):
		L.log('putfile startaction start :' + self.filename + ' : ' + str(self.filelen))
		try:
			self.file = open(self.filename, 'wb+')
		except Exception as e:
			L.log('open ' + self.filename + ' error')
			return ERR_OPEN_ERR 

		self.session.outbuf.append(putfileaction.RESPONSE_GETFILEINFO_OK)
		MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
		MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
		self.callback = self.handler_respgetfileinfook

	def handler_respgetfileinfook(self):
		# call parent's public sendreponse function 
		L.log('putfile handler_respgetfileinfook')
		if self.sendreponse() is not None:
			L.log('sendreponse error ')
			return ERR_RESP_ERR 	

		# next stage
		if not self.session.outbuf:
			MUTIIO.unregister(self.session.sockfd, MUTIIO_WRITE)
			MUTIIO.register(self.session.sockfd, MUTIIO_READ)
			self.callback = self.handler_recvingfiledata

	def handler_recvingfiledata(self):
		L.log('putfile handler_recvingfiledata')
		readlen = readfromsock(self.session.sock, self.session.inbuf)
		if readlen <= 0:
			L.log(" recving file data transfer error")
			return ERR_READ_ERR 
		else:
			# sync
			writelen = writetofile(self.file, self.session.inbuf)
			if readlen != writelen:
				L.log('ERROR: write disk not corrent!')

		self.filelen -= readlen
		
		if self.filelen <= 0:
			self.session.outbuf.append(putfileaction.RESPONSE_TRANSFERDATA_OK)
			MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
			MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
			self.callback = self.handler_resprecvdataok		

	def handler_resprecvdataok(self):
		L.log('putfile handler_resprecvdataok: ' + self.session.outbuf[0])
		# call parent's public sendreponse function 
		if self.sendreponse() is not None:
			L.log('sendreponse error ')
			return ERR_RESP_ERR

		if not self.session.outbuf:
			self.endaction()
			return		
	
'''
	process stage:
	1. client send command [get filename] and client waiting for server's getfileinfo 
        2. server parse command
        3. server send fileinfo[filename filelen]
        4. client get fileinfo and block to receive server filedata
        5. server send filedata
        6. when sendlen == filelen, client stop to send and server stop to receive
'''
class getfileaction(commandaction):
	RESPONSE_SENDFILEINFO_OK = 'FILEINFO OK\n'
	RESPONSE_TRANSFERDATA_OK = 'FILETRANSFER OK\n'

	def __init__(self, cmdname, session, cmdline):
		super(getfileaction, self).__init__(cmdname, session, cmdline)
		self.file = None
		self.filename = ''
		self.filelen = 0
		self.filedata = None
		self.eof = False
	
	def parsecmdline(self):
		L.log('getfile parsecmdline:' + self.cmdline)
		x = self.cmdline.split(' ')
		if len(x) <= 1:
			L.log('command:' + self.cmdline + 'format error')
			return ERR_FMT_ERR 
		else:
			self.filename = x[1]
			L.log('get cmdline: ' + self.filename) 

	def startaction(self):
		L.log('getfile startaction start :' + self.filename)
		try:
			statres = os.stat(self.filename)
			self.file = open(self.filename)
		except Exception as e:
			L.log('stat ' + self.filename + ' error')
			return ERR_OPEN_ERR

		self.filelen = statres.st_size
		responsefileinfo = self.filename + ' ' + str(self.filelen) + '\n'
		L.log('getfile responsefileinfo: ' + responsefileinfo) 
		self.session.outbuf.append(responsefileinfo)
		MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
		MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
		self.callback = self.handler_sendfileinfo

	def handler_sendfileinfo(self):
		# call parent's public sendreponse function 
		L.log('getfile handler_sendfileinfo: ' + self.session.outbuf[0]) 
		if self.sendreponse() is not None:
			L.log('sendreponse error ')
			return ERR_RESP_ERR

		if not self.session.outbuf:
			MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
			MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
			self.filedata = readfile(self.filename)
			if self.filedata is None:
				return ERR_READ_ERR 
			self.callback = self.handler_sendingfiledata

        def handler_sendingfiledata(self):
		L.log('getfile handler_sendingfiledata') 
		if not self.session.outbuf and self.eof == False and self.filelen > 0:
			try:
				tmpdata = self.filedata.next()
			except StopIteration as e:
				self.eof = True	
			except Exception as e:
				return ERR_READ_ERR 

			if self.eof == False:
				self.session.outbuf.append(tmpdata)

		sendlen = sendtosock(self.session.sock, self.session.outbuf)
		if sendlen <= 0:
			L.log(self.clientip + " " + self.filename + " transfer error")
			return ERR_WRITE_ERR
		else:
			self.filelen -= sendlen
		if self.filelen <= 0:
			self.endaction()
			return 	

### cd command 
'''
if cd ok: 	send "CDOD 'newabspath' 'promptends' \n"
else cd failed: send "CDFAIL path not exists\n"
'''
class cdaction(commandaction):
	def __init__(self, cmdname, session, cmdline):
		super(cdaction, self).__init__(cmdname, session, cmdline)
		self.relpath = ''

        def parsecmdline(self):
		L.log('cdaction parsecmdline: ' + self.cmdline)
		x = self.cmdline.split(' ')
		if len(x) == 1:
			self.relpath = BASEPATH 
		else:                   
			self.relpath = x[1]

	def startaction(self):
		L.log('cd startaction: ' + self.relpath)
                ''' join currentpath with relpath '''
                tmppath = os.path.join(self.session.currentpath, self.relpath)
                abspath = os.path.abspath(tmppath)

                if not os.path.exists(abspath):
                        self.session.outbuf.append(CDFAIL + ' ' + 'path not exists\n')
                else:
                        self.session.currentpath = abspath
                        self.session.outbuf.append(CDOK + ' ' +  self.session.currentpath + PROMPT_ENDS + '\n')

		L.log('now currentpath' + self.session.currentpath)
                MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
                MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
                self.callback = self.handler_respcdresult

	def handler_respcdresult(self):
		L.log('cd handler_respcdresult: ' + self.session.outbuf[0])
		if self.sendreponse() is not None:
			L.log('sendreponse error ')
                        return ERR_RESP_ERR

                if not self.session.outbuf:
			self.endaction()
                        return


class lsaction(commandaction):
	def __init__(self, cmdname, session, cmdline):
                super(lsaction, self).__init__(cmdname, session, cmdline)
		self.lspath = ''
		self.isabspath = False

	def parsecmdline(self):
		L.log('lsaction parsecmdline: ' + self.cmdline)
		x = self.cmdline.split(' ')
		if len(x) != 1:
			self.lspath = x[1]
			if x[1][0] == '/':
				self.isabspath = True
	
        def startaction(self):
		if self.isabspath == False:
			self.lspath = os.path.join(self.session.currentpath, self.lspath)

		self.lsabspath = os.path.abspath(self.lspath)

		L.log('cd startaction: ' + self.lsabspath + str(self.isabspath))

                try:
                        files = os.listdir(self.lsabspath)
                        for x in files:
                                self.session.outbuf.append(x + ' ')
                except OSError as e:
                        self.session.outbuf.append('path error!')

                self.session.outbuf.append('\n')

                MUTIIO.unregister(self.session.sockfd, MUTIIO_READ)
                MUTIIO.register(self.session.sockfd, MUTIIO_WRITE)
                self.callback = self.handler_resplsresult

                L.log('+++++startaction end++++')

	def handler_resplsresult(self):
		L.log('cd handler_resplsresult: ' + self.session.outbuf[0])
		if self.sendreponse() is not None:
			L.log('sendreponse error ')
                        return ERR_RESP_ERR

                if not self.session.outbuf:
			self.endaction()
                        return
	
COMMANDMAP = {  'put'   :       [CMD_PUT, putfileaction],
                'get'   :       [CMD_GET, getfileaction],
                'ls'    :       [CMD_LS, lsaction],
                'cd'    :       [CMD_CD, cdaction]              }

class session(object):
	def __init__(self, sock, clientip, clientport):
		self.clientip = clientip
		self.clientport = clientport
		self.sock = sock
		self.sockfd = self.sock.fileno()
		self.currentpath = BASEPATH 

		self.cmdact = None	
		self.cmdline = ''
		self.cmddeque = collections.deque()
		self.outbuf = collections.deque()
		self.inbuf = collections.deque()
		self.callback = None
	
	def closesession(self):
		MUTIIO.unregister(self.sockfd, MUTIIO_READ)
		MUTIIO.unregister(self.sockfd, MUTIIO_WRITE)
		MUTIIO.unregister(self.sockfd, MUTIIO_ERROR)
		if self.sock is not None:
			self.sock.close()

	def regetcommand(self):
		self.cmdact = None
		self.cmdline = ''
		self.cmddeque.clear()
		self.outbuf.clear()
		self.inbuf.clear()
		MUTIIO.unregister(self.sockfd, MUTIIO_WRITE)
		MUTIIO.unregister(self.sockfd, MUTIIO_ERROR)
		MUTIIO.register(self.sockfd, MUTIIO_READ)
		self.callback = self.handler_receivecmdline

	def handler_respconnectok(self):
		numbytes = sendtosock(self.sock, self.outbuf)
		if numbytes <= 0:
			L.log('handler_respconnectok sendtosock error')
			return ERR_WRITE_ERR 
		if not self.outbuf:
			MUTIIO.unregister(self.sockfd, MUTIIO_WRITE)
			MUTIIO.register(self.sockfd, MUTIIO_READ)
			self.callback = self.handler_receivecmdline
			
	def respconnectok(self):
                self.outbuf.append(BASEPATH + PROMPT_ENDS + '\n')
		MUTIIO.register(self.sockfd, MUTIIO_WRITE)
		MUTIIO.unregister(self.sockfd, MUTIIO_READ)
		self.callback = self.handler_respconnectok
		
	def handler_receivecmdline(self):
		readlen = readfromsock(self.sock, self.cmddeque)
		if readlen <= 0:
			L.log(self.clientip + ' read command line error')
			return ERR_READ_ERR 
	
		mergeprefix(self.cmddeque, 1000)

		self.cmdline = self.cmddeque[0]
		if self.cmdline.find('\n') != -1:
			if len(self.cmdline) > 256:
				self.recvcmdlinetolong()
				L.log('command line too long\n')
				return ERR_TOO_LONG 
				
			# earse \n 
			self.cmdline = self.cmdline[ : len(self.cmdline) - 1 ]

			L.log('RECV new cmdline: ' + self.cmdline)

			cmd = self.cmdline.split(' ')
			cmdmap = COMMANDMAP.get(cmd[0])
			print cmd[0]
			if cmdmap is None:
				L.log('Unknow command:(' + cmd[0] + ')')
				return ERR_FMT_ERR 
			else:
				self.cmdact = cmdmap[1](cmdmap[0], self, self.cmdline)
				if self.cmdact.parsecmdline() is not None:
					L.log('parse command line error: ' + self.cmdline)
					self.regetcommand()
					return ERR_FMT_ERR

			self.callback = None
			if self.cmdact.startaction() is not None:
				L.log('start command action error: ' + self.cmdline)
				self.regetcommand()

			L.log('cmdtype :' + str(cmd))
		else:
			if len(self.cmdline) > 256:
				self.recvcmdlinetolong()
				L.log('command line too long\n')
				return ERR_TOO_LONG 

	def sessioncontrol(self):
		L.log('<<<<sessioncontrol start>>>>> ' + str(self.callback))
		L.log('<<<<<commandaction start>>>>> ' + 'None' if self.cmdact is None else str(self.cmdact.callback))

		if self.callback is not None:
			ret = self.callback()
			if ret is not None:
				L.log(str(self.callback) + 'callback error, errno:' + str(ret))
				return ERR_INTERNAL_ERR
		else:	
			assert self.cmdact is not None

			ret = self.cmdact.callback()
			if ret is not None:
				L.log('command action error: ' + self.cmdline + str(ret))
				self.cmdact.endaction(successflag = False)

		L.log('<<<<<sessioncontrol end>>>>>> ' + str(self.callback))
		L.log('<<<<<commandaction start>>>>> ' + ('None' if self.cmdact is None else str(self.cmdact.callback)))

class selectio(object):
	def __init__(self):
		self.read_fds = set()
		self.write_fds = set()
		self.error_fds = set()
		self.fd_sets = (self.read_fds, self.write_fds, self.error_fds)

	def register(self, fd, events):
		if fd in self.read_fds or fd in self.write_fds or fd in self.error_fds:
			return
		if events & MUTIIO_READ:
		    self.read_fds.add(fd)
		if events & MUTIIO_WRITE:
		    self.write_fds.add(fd)
		if events & MUTIIO_ERROR:
		    self.error_fds.add(fd)

	def unregister(self, fd, events):
		if events & MUTIIO_READ:
			self.read_fds.discard(fd)
		if events & MUTIIO_WRITE:
			self.write_fds.discard(fd)
		if events & MUTIIO_ERROR:
			self.error_fds.discard(fd)
		
	def poll(self, timeout = 0):
		readable, writeable, errors = select.select( self.read_fds, self.write_fds, self.error_fds, timeout / 1000)
		events = {}	# {fd, READ | WRITE | ERROR}
		for fd in readable:
			events[fd] = events.get(fd, 0) | MUTIIO_READ
		for fd in writeable:
			events[fd] = events.get(fd, 0) | MUTIIO_WRITE
		for fd in errors:
			events[fd] = events.get(fd, 0) | MUTIIO_ERROR
		return events.items()


class socketserver(object):
	workingsession = {} 
	listensockets = []

	def __init__(self, port = 19898):
		self.port = port

	def init(self):
		try:
			sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
		except socket.error as e:
			return ERR_OPEN_ERR 
			
		sockaddr = ('', self.port)
		sock.setblocking(0)
		sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
		sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

		sock.bind(sockaddr)
		sock.listen(self.port)
		socketserver.listensockets.append(sock)


	def accepthandler(self, connection, address):
		L.log('client[' + address[0] + ':' + str(address[1]) + ' ] accept')
		newsess = session(connection, address[0], address[1])
		socketserver.workingsession[connection.fileno()] = newsess 
		connection.setblocking(0)
		connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
		connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		newsess.respconnectok()
		L.log('client[' + address[0] + ':' + str(address[1]) + ' ] session inited')

	def loop(self):
		while True:
			for sock in socketserver.listensockets:
				while True:
					try:
						connection, address = sock.accept()
						self.accepthandler(connection, address)
					except Exception as e:
						if geterrnofrome(e) in ERRNO_WOULDBLOCK:
							break
						if geterrnofrome(e) == errno.ECONNABORTED:
							continue;
						else:
							raise

				events = MUTIIO.poll(100)	#ms
				for event in events:
					sess = socketserver.workingsession[event[0]]

					print '++++++++++++++++++++++++++++++++++++++++++++++++'
					if event[1] & MUTIIO_ERROR:
						print 'get error'
						L.log('ERROR event: ' + str(event[0]))
						sess.closesession()
						socketserver.workingsession.pop(event[0])	

					else: 
						if event[1] & MUTIIO_READ:
							L.log('READ event: ' + str(event[0]))
						else:
							L.log('WRITE event: ' + str(event[0]))

						if sess.sessioncontrol() is not None:
							print 'error:' + sess.clientip + str(sess.clientport) + ': error'
							sess.closesession()
							socketserver.workingsession.pop(event[0])	

					print '------------------------------------------------'

			if not socketserver.workingsession:
				time.sleep(1)
						
						
MUTIIO = selectio()

if __name__ == '__main__':			
	if os.access(BASEPATH, os.R_OK | os.W_OK) == False:
		L.log('ERROR: basepath: ' + BASEPATH + ' not correct!')	
		exit(0)

	serverfile  = socketserver()
	if serverfile.init() is not None:
		L.log('server init error!')	
	L.log('server init ok')
	serverfile.loop()
